#include "core/BlackboardManager.h"
#include "core/state/States.h"
#include "network/packet/Packets.h"

#include <QPainter>

BlackboardManager::BlackboardManager()
    : blackboardState(NULL), currentDrawPath(NULL), hasMutualLock(false), requestMutualLock(false) {
    nodeList = new NodeList();

    paintTimer = new QTimer(this);
    nodeStatusCheckTimer = new QTimer(this);
    mutualExclusionCheckTimer = new QTimer(this);

    mutualExclusionCheckTimer->setInterval(500);

    connect(paintTimer, SIGNAL(timeout()), this, SLOT(paintTimerEvent()));
    connect(nodeStatusCheckTimer, SIGNAL(timeout()), this, SLOT(checkNodesStatus()));
    connect(mutualExclusionCheckTimer, SIGNAL(timeout()), this, SLOT(checkMutualExclusion()));
    connect(&connection, SIGNAL(packetReceived(AbstractPacket*)), this, SLOT(packetReceived(AbstractPacket*)));
    connect(&connection, SIGNAL(socketClosed(QString,quint16)), this, SLOT(connectionClosed(QString,quint16)));

    lastMutualExclusionTokenReceivedTime = mutalExclusionStartTime = QTime::currentTime();

    nodeStatusCheckTimer->start();
    mutualExclusionCheckTimer->start();
}

BlackboardManager::~BlackboardManager() {
    delete nodeList;
}

void BlackboardManager::initNew(const QString & address,
                                quint16 port,
                                QColor penColor,
                                int width,
                                int height) {
    blackboardState = BlackboardStateRunning::instance();

    Node *thisNode = new Node(Node::generateUniqueId(), address, port, penColor);
    nodeList->setThisNode(thisNode);
    nodeList->addNode(*thisNode);

    setSize(width, height);

    connection.init(address, port);

    initPainting();
}

void BlackboardManager::joinSpecific(const QString & listenAddres,
                                     quint16 listenPort,
                                     QColor penColor,
                                     const QString & joinAddress,
                                     quint16 joinPort) {
    blackboardState = BlackboardStateJoining::instance();
    ((BlackboardStateJoining *) blackboardState)->setJoiningNode(joinAddress, joinPort);

    Node *thisNode = new Node(Node::generateUniqueId(), listenAddres, listenPort, penColor);
    nodeList->setThisNode(thisNode);

    connection.init(listenAddres, listenPort);

    JoinRequestPacket joinPacket(listenAddres, listenPort, penColor);
    joinPacket.setReceiver(joinAddress, joinPort);
    sendPacket(&joinPacket);
}

void BlackboardManager::packetReceived(AbstractPacket * packet) {
    blackboardState->packetReceived(this, packet);

    delete packet;
}

void BlackboardManager::sendPacket(AbstractPacket * packet) {
    const Node & thisNode = nodeList->getThisNode();
    packet->setSenderId(thisNode.getId());

    // special case: sending packet to yourself
    if (packet->getReceiverAddress() == thisNode.getAddress()
            && packet->getReceiverPort() == thisNode.getPort()) {
        blackboardState->packetReceived(this, packet);
        return;
    }
    connection.sendPacket(packet);
}

void BlackboardManager::setSize(int width, int height) {
    this->width = width;
    this->height = height;
    emit sizeChanged(width, height);

    blackboardImage = new QImage(width, height, QImage::Format_RGB888);
    blackboardImage->fill(qRgb(255, 255, 255));
}

void BlackboardManager::switchState(BlackboardState *nextState) {
    qDebug() << "Switching state from " << blackboardState->getStateName()
             << " to " << nextState->getStateName();
    blackboardState = nextState;
}

void BlackboardManager::processAndSendPaintPacket(PaintPacket * paintPacket) {
    const QList<DrawPath *> * drawPaths = paintPacket->getDrawPaths();
    Node & senderNode = nodeList->getNode(paintPacket->getSenderId());
    QColor paintColor = senderNode.getPaintColor();
    senderNode.updateLastSeenTime();

    QPainter painter(blackboardImage);
    painter.setPen(QPen(paintColor));
    for (int i = 0; i < drawPaths->length(); ++i) {
        const QList<QPoint> pointList = drawPaths->at(i)->getPointList();
        QPoint last = pointList.at(0);
        for (int j = 1; j < pointList.length(); ++j) {
            QPoint current = pointList.at(j);
            painter.drawLine(last, current);
            last = current;
        }
    }

    if (paintPacket->getSenderId() != getThisNode().getId()) {
        emit pathsReceived(drawPaths, paintColor);

        paintPacket->setReceiver(&getNextNode());
        connection.sendPacket(paintPacket);
    }
}

void BlackboardManager::processMutualExclusionTokenPacket(MutualExclusionTokenPacket *packet) {
    lastMutualExclusionTokenReceivedTime = QTime::currentTime();

    const MutualExclusionToken token = packet->getToken();
    if (token.isLocked()) {
        if (nodeList->getLockedId() != token.getOwnerId()) {
            nodeList->setLockedId(token.getOwnerId());
            mutalExclusionStartTime = QTime::currentTime();
        } else if (nodeList->getLockedId() == getThisNode().getId()
                   && requestMutualUnlock) {
            hasMutualLock = false;
            requestMutualUnlock = false;
            nodeList->setLockedId("");
            mutualExclusionToken = MutualExclusionToken();
            QTimer::singleShot(10, this, SLOT(sendMutualExclusionToken()));
            return;
        }

        mutualExclusionToken = token;
        QTimer::singleShot(500, this, SLOT(sendMutualExclusionToken()));
    } else {
        if (requestMutualLock) {
            mutualExclusionToken = MutualExclusionToken(getThisNode().getId());
            requestMutualLock = false;
            hasMutualLock = true;
            nodeList->setLockedId(getThisNode().getId());
            QTimer::singleShot(10, this, SLOT(sendMutualExclusionToken()));
        } else {
            nodeList->setLockedId("");
            mutualExclusionToken = MutualExclusionToken();
            QTimer::singleShot(500, this, SLOT(sendMutualExclusionToken()));
        }
    }

    if (!nodeList->getLockedId().isEmpty() && !hasMutualLock) {
        emit lockPaintChanged(true);
    } else {
        emit lockPaintChanged(false);
    }
}

void BlackboardManager::sendMutualExclusionToken() {
    MutualExclusionTokenPacket packet(mutualExclusionToken);
    packet.setReceiver(&getNextNode());
    sendPacket(&packet);
}

void BlackboardManager::requestLock() {
    if (hasMutualLock) {
        return;
    }
    requestMutualLock = true;
    requestMutualUnlock = false;
}

void BlackboardManager::requestUnlock() {
    if (!hasMutualLock) {
        return;
    }
    requestMutualLock = false;
    requestMutualUnlock = true;
}

void BlackboardManager::initPainting() {
    paintTimer->start(1000);
}

void BlackboardManager::paintTimerEvent() {
    QList<DrawPath *> * sendDrawPaths = getCurrentDrawPaths();

    PaintPacket paintPacket(*sendDrawPaths);
    paintPacket.setReceiver(&getNextNode());
    sendPacket(&paintPacket);

    for (int i = 0; i < sendDrawPaths->length(); ++i) {
        delete sendDrawPaths->at(i);
    }
    delete sendDrawPaths;
}

QList<DrawPath *> * BlackboardManager::getCurrentDrawPaths() {
    if (currentDrawPath != NULL && currentDrawPath->getPointList().length() > 1) {
        userDrawPaths.append(currentDrawPath);
        QPoint lastPoint = currentDrawPath->getLastPoint();
        currentDrawPath = new DrawPath();
        currentDrawPath->addPoint(lastPoint);
    }
    QList<DrawPath *> *retList = new QList<DrawPath *>(userDrawPaths);
    userDrawPaths.clear();
    return retList;
}

void BlackboardManager::pointDrawed(QPoint point) {
    if (currentDrawPath == NULL) {
        currentDrawPath = new DrawPath();
    }
    if (point.x() == -1 && point.y() == -1) {
        QPoint lastPoint = currentDrawPath->getLastPoint();
        userDrawPaths.append(currentDrawPath);
        currentDrawPath = new DrawPath();
    } else {
        currentDrawPath->addPoint(point);
    }
}

void BlackboardManager::connectionClosed(const QString & address, quint16 port) {
    const Node & node = nodeList->getNode(address, port);
    nodeList->removeNode(node);

    qDebug() << "Removing node listening on " << address << ":" << port;
}

void BlackboardManager::checkNodesStatus() {
    const QTime currTime = QTime::currentTime();
    const QList<Node> nodes = nodeList->innerList();
    for (QList<Node>::const_iterator it = nodes.begin(); it != nodes.end(); ++it) {
        const Node node = *it;
        if (node == getThisNode()) {
            continue;
        }

        const QTime lastSeenTime = node.getLastSeenTime();
        if (lastSeenTime.msecsTo(currTime) > NODE_TIMEOUT) {
            nodeList->removeNode(node);

            qDebug() << "Node timeout, currTime: " << currTime << ", lastSeenTime: " << lastSeenTime;
        }
    }
}

void BlackboardManager::checkMutualExclusion() {
    QTime currTime = QTime::currentTime();
    // check if mutual exclusion token lost and create new if necessary
    if (lastMutualExclusionTokenReceivedTime.msecsTo(currTime) > MUTUAL_EXCLUSION_TOKEN_TIMEOUT) {
        if (nodeList->isThisNodeFirst()) {
            MutualExclusionToken token;
            MutualExclusionTokenPacket packet(token);
            packet.setReceiver(&getNextNode());
            sendPacket(&packet);
        }
    }
}

const QImage & BlackboardManager::getCurrentBlackboardImage() {
    return *blackboardImage;
}

void BlackboardManager::resetImage(const QImage & image) {
    if (blackboardImage != NULL) {
        delete blackboardImage;
    }
    blackboardImage = new QImage(image);

    emit imageReceived(image);
}
